package idgenerator

import (
	"codeup.aliyun.com/5f9118049cffa29cfdd3be1c/util"
	"context"
	"fmt"
	"github.com/godruoyi/go-snowflake"
	"github.com/pkg/errors"
	"github.com/redis/go-redis/v9"
	"sync"
	"time"
)

type Generator struct {
	redisClient *redis.Client
	redisPreKey string
	machineID   uint16
	log         util.Logger
	ctx         context.Context
}

// singleIdGenerator 单例ID生成器
var singleIdGenerator *Generator

// 控制单例锁
var once = new(sync.Once)

// minHoldTime 最小占用时间，超过这个时间没有续约的机器ID将被释放
const minHoldTime = 10 * time.Minute
const tickerInterval = 1 * time.Minute

// NewGenerator 创建ID生成器
// log 一般为 *log.Helper类型，可以传nil，传nil时会使用fmt.Printf输出日志
// redisClient Redis客户端
// redisPreKey Redis key的前缀，建议使用名称：{项目名}_id_generator_util
func NewGenerator(log util.Logger, redisClient *redis.Client, redisPreKey string) *Generator {
	once.Do(func() {
		var err error
		singleIdGenerator = &Generator{ctx: context.Background(), log: log, redisClient: redisClient, redisPreKey: redisPreKey}
		singleIdGenerator.machineID, err = singleIdGenerator.setMachineID()
		if err != nil {
			panic(fmt.Sprintf("设置机器ID失败：%v", err))
		}
	})
	if singleIdGenerator.redisClient != redisClient || singleIdGenerator.redisPreKey != redisPreKey {
		panic("只能创建一个ID生成器，不能重复创建")
	}
	return singleIdGenerator
}

func (g *Generator) GetMachineID() uint16 {
	return g.machineID
}

// GetOne 获取一个Id，每毫秒可生成4096个ID（由雪花算法决定），毫秒内超过会阻塞
func (g *Generator) GetOne() int {
	return int(snowflake.ID())
}

// GetN 获取N个Id，每毫秒可生成4096个ID（由雪花算法决定），毫秒内超过会阻塞
func (g *Generator) GetN(n int) []int {
	ids := make([]int, n)
	for j := 0; j < n; j++ {
		ids[j] = g.GetOne()
	}
	return ids
}

// setMachineID 设置机器ID
func (g *Generator) setMachineID() (machineId uint16, err error) {
	var ok bool
	retryTimes := 100
	for j := 0; j < retryTimes; j++ {
		// 取机器ID序号
		machineId, err = singleIdGenerator.getMachineID()
		if err != nil {
			return 0, errors.WithMessage(err, "获取机器ID失败")
		}
		// 检查机器ID是否占用
		ok, err = singleIdGenerator.checkAndHoldMachineID(machineId)
		if err != nil {
			return 0, errors.WithMessage(err, "检查机器ID是否占用失败")
		}
		if ok {
			break
		}
		g.logInfo("id生成器机器的机器ID %d 已被占用，尝试重新获取", machineId)
	}
	if !ok {
		return 0, errors.Errorf("尝试%d次，机器ID都被占用，请确认redis中的数据是否异", retryTimes)
	}
	snowflake.SetMachineID(machineId)
	g.logInfo("已设置id生成器机器的机器ID为 %d", machineId)
	return machineId, nil
}

// getMachineID 获取机器ID
func (g *Generator) getMachineID() (uint16, error) {
	// 自增ID序号当作机器ID
	machineIdKey := g.getMachineIdSeqKey(g.redisPreKey)
	sequenceNumber, err := g.redisClient.Incr(context.Background(), machineIdKey).Result()
	if err != nil {
		return 0, err
	}
	machineId := uint16(sequenceNumber) % (snowflake.MaxMachineID + 1)
	return machineId, nil
}

// getMachineIdSeqKey 获取机器ID序号的Redis key
func (g *Generator) getMachineIdSeqKey(redisPreKey string) string {
	return redisPreKey + ":machine_id_sequence"
}

// checkAndHoldMachineID 检查并占用机器ID
func (g *Generator) checkAndHoldMachineID(machineId uint16) (bool, error) {
	machineIdHoldKey := g.getAliveKey(machineId)
	res := g.redisClient.SetNX(g.ctx, machineIdHoldKey, 1, minHoldTime)
	err := res.Err()
	if err != nil {
		return false, errors.WithMessage(err, "设置机器码被已使用状态失败")
	}
	isOk := res.Val()
	if isOk {
		// 保持存活状态
		go g.keepAlive(machineIdHoldKey)
	}
	return isOk, nil
}

// keepAlive 保持机器ID存活
func (g *Generator) keepAlive(key string) {
	timer := time.NewTicker(tickerInterval)
	defer timer.Stop()
	times := 0
	for {
		select {
		case <-timer.C:
			times++
			statusCmd := g.redisClient.SetEx(context.Background(), key, times, minHoldTime)
			if statusCmd.Err() != nil {
				g.logError("续约机器ID失败：%v", statusCmd.Err())
			}
		}
	}
}

// logInfo 输出info日志
func (g *Generator) logInfo(format string, args ...interface{}) {
	if g.log == nil {
		fmt.Printf(format+"\n", args)
		return
	}
	g.log.Infof(format, args)
}

// logError 输出错误日志
func (g *Generator) logError(format string, args ...interface{}) {
	if g.log == nil {
		fmt.Printf(format+"\n", args)
		return
	}
	g.log.Errorf(format, args)
}

// getAliveKey 获取机器ID存活状态的Redis key
func (g *Generator) getAliveKey(machineId uint16) string {
	return fmt.Sprintf("%s:alive_%d", g.redisPreKey, machineId)
}
